package ru.housewife.form;

import org.springframework.validation.Errors;
import ru.housewife.cache.CacheManager;
import ru.housewife.model.*;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * class form for user
 *
 * @author Alexander Troshanin
 */
public class UserForm extends AbstractFormObject<UserForm> {

    private long id;

    private String userName;

    private String userPass;

    private String fullName;

    private boolean sex;

    private String email;

    private RegionForm region;

    private Map<Long, UsersTableForm> usersTables;

    private List<UsersTableForm> usersTablesSortedCache;

    private Map<Long, UsersRecipe> userRecipes;

    private List<UsersRecipe> userRecipesSortedCache;

    private Map<Long, UsersRecipeGroup> usersRecipesGroups;
    private SortedSet<UsersRecipeGroup> firstLevelUsersRecipesGroupsCache;
    private UsersRecipeGroup rootZeroGroup;

    private Map<Long, WareUserPriceForm> waresUserPrices;

    private Map<Long, PreferableWareForComponentForm> preferableWaresForComponents;

    private Map<Long, UsersWare> usersWares;
    private List<UsersWare> usersWaresSortedCache;

    private List<UsersTableBriefBlockForm> usersTableBriefBlocksCache;

    private boolean needToReloadPrices;

    private Lock lock = new ReentrantLock();

    public UserForm() {
        region = new RegionForm();
    }

    public UserForm(User user) {
        id = user.getId();
        userName = user.getUserName();
        userPass = user.getUserPass();
        fullName = user.getFullName();
        sex = user.getSex();
        email = user.getEmail();
        region = user.getRegion() != null ? CacheManager.getInstance().getRegionForm(user.getRegion().getId()) : null;

        userRecipes = new HashMap<Long, UsersRecipe>();
        for (CookingDishRecipe recipe : user.getUserRecipes()) {
            CookingDishRecipeForm recipeForm = null;
            if (recipe.isCommonRecipe()) {
                recipeForm = CacheManager.getInstance().getCookingDishRecipeForm(recipe.getId());
            }
            if (recipeForm == null) {
                recipeForm = new CookingDishRecipeForm(recipe);
            }
            userRecipes.put(recipeForm.getId(), new UsersRecipe(recipeForm));
        }
        userRecipesSortedCache = generateSortedCache(userRecipes);

        usersTables = new HashMap<Long, UsersTableForm>();
        for (UsersTable ut : user.getUsersTables()) {
            UsersTableForm utf = new UsersTableForm(this, ut);
//            utf.setUser(this);
            usersTables.put(utf.getId(), utf);
        }
        usersTablesSortedCache = generateSortedCache(usersTables);

        waresUserPrices = new HashMap<Long, WareUserPriceForm>();
        for (WareUserPrice wareUserPrice : user.getWaresUserPrices()) {
            WareForm wareForm = CacheManager.getInstance().getWareForm(wareUserPrice.getWare().getId());
            if (wareForm != null) {
                WareUserPriceForm frm = new WareUserPriceForm(wareUserPrice.getId(), wareForm, this, wareUserPrice.getPrice());
                waresUserPrices.put(frm.getWare().getId(), frm);
            } else {
                // TODO: ALARM
            }
        }

        preferableWaresForComponents = new HashMap<Long, PreferableWareForComponentForm>();
        for (PreferableWareForComponent preferableWareForComponent : user.getPreferableWaresForComponents()) {
            PreferableWareForComponentForm preferableWareForComponentForm = new PreferableWareForComponentForm(this, preferableWareForComponent);
            preferableWaresForComponents.put(preferableWareForComponentForm.getComponentId(), preferableWareForComponentForm);
        }

        generateUsersRecipesGroups();

        generateUsersWares();
    }

    public void regenerateUsersWares() {
        generateUsersWares();
    }

    private void generateUsersWares() {
        usersWares = new HashMap<Long, UsersWare>();
        for (UsersRecipe userRecipe : userRecipesSortedCache) {
            addRecipesWaresToUsersWares(userRecipe.recipe, false);
        }
        for (UsersTableForm utf : usersTablesSortedCache) {
            for (DayMenuForm dmf : utf.getDayMenus()) {
                for (EatingMenuForm emf : dmf.getEatingMenus()) {
                    for (EatingMenuDishRecipeForm emrf : emf.getEatingMenuRecipes()) {
                        addRecipesWaresToUsersWares(emrf.getRecipe(), false);
                    }
                }
            }
        }
        for (WareUserPriceForm userPrice : waresUserPrices.values()) {
            WareForm ware = userPrice.getWare();
            UsersWare userWare = usersWares.get(ware.getId());
            if (userWare == null) {
                userWare = new UsersWare(ware, 0);
                usersWares.put(ware.getId(), userWare);
            }
            userWare.setWareUserPrice(userPrice);
        }
        for (PreferableWareForComponentForm preferableWareForComponent : preferableWaresForComponents.values()) {
            addPreferableWareForComponentToUsersWares(preferableWareForComponent);
        }
        usersWaresSortedCache = generateSortedCache(usersWares);
        CacheManager.getInstance().fillWaresRegionPricesForUser(this);
    }

    private void addRecipesWaresToUsersWares(CookingDishRecipeForm recipe, boolean regenerateCache) {
        boolean mapChanged = false;
        for (CookingDishComponentForm component : recipe.getComponents()) {
            WareForm ware = component.getPreferableWare();
            if (addWareToUsersWaresMap(ware)) {
                mapChanged = true;
            }
        }
        if (regenerateCache && mapChanged) {
            usersWaresSortedCache = generateSortedCache(usersWares);
        }
    }

    private boolean addWareToUsersWaresMap(WareForm ware) {
        boolean mapChanged = false;
        UsersWare usersWare = usersWares.get(ware.getId());
        if (usersWare == null) {
            usersWare = new UsersWare(ware);
            usersWares.put(ware.getId(), usersWare);
            mapChanged = true;
        } else {
            usersWare.incCount(1);
        }
        return mapChanged;
    }

    public void addRecipesWaresToUsersWares(CookingDishRecipeForm recipe) {
        addRecipesWaresToUsersWares(recipe, true);
        CacheManager.getInstance().fillWaresRegionPricesOfRecipeForUser(this, recipe);
    }

    public void removeRecipesWaresFromUsersWares(CookingDishRecipeForm recipe) {
        boolean mapChanged = false;
        for (CookingDishComponentForm component : recipe.getComponents()) {
            WareForm ware = component.getPreferableWare();
            UsersWare usersWare = usersWares.get(ware.getId());
            if (usersWare != null) {
                mapChanged = removeUsersWareFromMap(usersWare);
            } else {
                // TODO: ALARM
                System.out.println("[ALARM] ware is not in user's wares removeRecipesWaresFromUsersWares()");
            }
        }
        if (mapChanged) {
            usersWaresSortedCache = generateSortedCache(usersWares);
        }
    }

    private boolean removeUsersWareFromMap(UsersWare usersWare) {
        boolean mapChanged = false;
        WareForm ware = usersWare.getWare();
        if (usersWare.getCount() > 1) {
            usersWare.decCount(1);
        } else {
            if (usersWare.getWareUserPrice() != null) {
                usersWare.decCount(1);
            } else {
                usersWares.remove(ware.getId());
                mapChanged = true;
            }
        }
        return mapChanged;
    }

    private void generateUsersRecipesGroups() {
        usersRecipesGroups = new HashMap<Long, UsersRecipeGroup>();
        firstLevelUsersRecipesGroupsCache = new TreeSet<UsersRecipeGroup>();
        for (UsersRecipe userRecipe : userRecipesSortedCache) {

            CookingDishRecipeForm recipeForm = userRecipe.recipe;

            CookingDishRecipeGroupForm group = recipeForm.getGroup();
            UsersRecipeGroup usersRecipeGroup = usersRecipesGroups.get(group.getId());
            if (usersRecipeGroup == null) {
                usersRecipeGroup = new UsersRecipeGroup(group);
                usersRecipesGroups.put(usersRecipeGroup.getId(), usersRecipeGroup);

                UsersRecipeGroup curUsersRecipeGroup = usersRecipeGroup;
                boolean exit = false;
                while (!exit) {

                    CookingDishRecipeGroupForm parent = group.getParent();
                    if (parent == null) {
                        // this is first level group!!!
                        firstLevelUsersRecipesGroupsCache.add(curUsersRecipeGroup);
                        exit = true;
                    } else {
                        UsersRecipeGroup usersRecipeParentGroup = usersRecipesGroups.get(parent.getId());
                        if (usersRecipeParentGroup == null) {
                            usersRecipeParentGroup = new UsersRecipeGroup(parent);
                            usersRecipesGroups.put(usersRecipeParentGroup.getId(), usersRecipeParentGroup);

                            usersRecipeParentGroup.addChildGroup(curUsersRecipeGroup);
                            curUsersRecipeGroup.setParent(usersRecipeParentGroup);

                            curUsersRecipeGroup = usersRecipeParentGroup;
                            group = parent;
                        } else {
                            usersRecipeParentGroup.addChildGroup(curUsersRecipeGroup);
                            curUsersRecipeGroup.setParent(usersRecipeParentGroup);
                            exit = true;
                        }
                    }

                }
            }
            usersRecipeGroup.addRecipe(recipeForm);
        }
        rootZeroGroup = new UsersRecipeGroup(null);
        for (UsersRecipeGroup gr : firstLevelUsersRecipesGroupsCache) {
            rootZeroGroup.addChildGroup(gr);
        }
    }

    public void updateUsersRecipesGroups() {
        generateUsersRecipesGroups();
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserPass() {
        return userPass;
    }

    public void setUserPass(String userPass) {
        this.userPass = userPass;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public boolean isSex() {
        return sex;
    }

    public void setSex(boolean sex) {
        this.sex = sex;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public RegionForm getRegion() {
        return region;
    }

    public void setRegion(RegionForm region) {
        this.region = region;
    }

    public List<UsersTableForm> getUsersTables() {
        return Collections.unmodifiableList(usersTablesSortedCache);
    }

    public UsersTableForm getUsersTable(long usersTableId) {
        return usersTables.get(usersTableId);
    }

    public void addUsersTable(UsersTableForm usersTableForm) {
        usersTableForm.setUser(this);
        usersTables.put(usersTableForm.getId(), usersTableForm);
        generateSortedCache(usersTablesSortedCache, usersTables);
    }

    public void removeUsersTable(long usersTableId) {
        usersTables.remove(usersTableId);
        generateSortedCache(usersTablesSortedCache, usersTables);
        generateUsersWares();
    }

    public List<UsersRecipe> getUserRecipes() {
        return Collections.unmodifiableList(userRecipesSortedCache);
    }

    public UsersRecipe getUserRecipe(long recipeId) {
        return userRecipes.get(recipeId);
    }

    public CookingDishRecipeForm getUsersCookingDishRecipeForm(long recipeId) {
        UsersRecipe userRecipe = getUserRecipe(recipeId);
        return userRecipe != null ? userRecipe.recipe : null;
    }

    public void addUserRecipe(CookingDishRecipeForm recipeForm) {
//        usersTableForm.setUser(this);
        userRecipes.put(recipeForm.getId(), new UsersRecipe(recipeForm));
        addRecipesWaresToUsersWares(recipeForm);
        generateSortedCache(userRecipesSortedCache, userRecipes);
        generateUsersRecipesGroups();
    }

    public void removeUserRecipe(long recipeId) {
        UsersRecipe userRecipe = userRecipes.remove(recipeId);
        if (userRecipe != null) {
            removeRecipesWaresFromUsersWares(userRecipe.recipe);
            generateSortedCache(userRecipesSortedCache, userRecipes);
            generateUsersRecipesGroups();
            for (EatingMenuDishRecipeForm emrf : userRecipe.getEatingMenuRecipes().values()) {
                emrf.getEatingMenu().removeEatingMenuRecipe(emrf.getId());
            }
        }
    }

    public SortedSet<UsersRecipeGroup> getFirstLevelUsersRecipesGroups() {
        return firstLevelUsersRecipesGroupsCache;
    }

    public UsersRecipeGroup getRootZeroGroup() {
        return rootZeroGroup;
    }

    public Map<Long, UsersRecipeGroup> getUsersRecipesGroups() {
        return usersRecipesGroups;
    }

    public UsersWare getUsersWare(long wareId) {
        return usersWares.get(wareId);
    }

    public Map<Long, UsersWare> getUsersWaresMap() {
        return Collections.unmodifiableMap(usersWares);
    }

    public List<UsersWare> getUsersWares() {
        return usersWaresSortedCache;
    }

    public void checkUsersWaresPrices() {
        if (needToReloadPrices) {
            CacheManager.getInstance().fillWaresRegionPricesForUser(this);
            needToReloadPrices = false;
        }
    }

    public WareUserPriceForm getWareUserPrice(long wareId) {
        return waresUserPrices.get(wareId);
    }

    public void addWareUserPrice(WareUserPriceForm userPrice) {
        long wareId = userPrice.getWare().getId();
        UsersWare userWare = usersWares.get(wareId);
        if (userWare == null) {
            // This may happens when user edits some recipe and adds some ware but ot commits recipe. And for this ware he sets price! TODO: test it
            userWare = new UsersWare(userPrice.getWare(), 0);
            usersWares.put(wareId, userWare);

            usersWaresSortedCache = generateSortedCache(usersWares);
        }
        userWare.setWareUserPrice(userPrice);
        waresUserPrices.put(wareId, userPrice);
    }

    public Map<Long, WareUserPriceForm> getWareUserPrices() {
        return Collections.unmodifiableMap(waresUserPrices);
    }

    public void setNeedToReloadPrices(boolean needToReloadPrices) {
        this.needToReloadPrices = needToReloadPrices;
    }

    public Map<Long, PreferableWareForComponentForm> getPreferableWaresForComponents() {
        return Collections.unmodifiableMap(preferableWaresForComponents);
    }

    public PreferableWareForComponentForm getPreferableWareForComponent(long componentId) {
        return preferableWaresForComponents.get(componentId);
    }

    public void addPreferableWareForComponent(PreferableWareForComponentForm preferableWareForComponent) {
        preferableWaresForComponents.put(preferableWareForComponent.getComponentId(), preferableWareForComponent);
        addPreferableWareForComponentToUsersWares(preferableWareForComponent);
    }

    public void addPreferableWareForComponentToUsersWares(PreferableWareForComponentForm preferableWareForComponent) {
        WareForm ware = preferableWareForComponent.getWare();
        if (addWareToUsersWaresMap(ware)) {
            usersWaresSortedCache = generateSortedCache(usersWares);
        }
    }

    public void removePreferableWareForComponentFromUsersWares(PreferableWareForComponentForm preferableWareForComponent) {
        WareForm ware = preferableWareForComponent.getWare();
        UsersWare userWare = usersWares.get(ware.getId());
        if (userWare != null) {
            if (removeUsersWareFromMap(userWare)) {
                usersWaresSortedCache = generateSortedCache(usersWares);
            }
        }
    }

    public void validate(Errors errors) {
        userName = userName != null ? userName.trim() : null;
        if (userName == null || userName.length() < 3) {
            errors.rejectValue("userName", "registration.fail.username.bad.format");
        }
        if (userPass == null || userPass.length() < 3) {
            errors.rejectValue("userPass", "registration.fail.password.bad.format");
        }
        if (email == null || !email.matches(".+@.+\\.[a-z]+")) {
            errors.rejectValue("email", "registration.fail.email.bad.format");
        }
        if (region.getId() != 0) {
            region = CacheManager.getInstance().getRegionForm(region.getId());
            if (region == null || !region.isSelectable()) {
                region = new RegionForm();
                errors.rejectValue("region", "registration.fail.bad.region");
            }
        }

    }

    public List<UsersTableBriefBlockForm> getUsersTableBriefBlocks() {
        return usersTableBriefBlocksCache;
    }

    public void setUsersTableBriefBlocks(List<UsersTableBriefBlockForm> usersTableBriefBlocks) {
        this.usersTableBriefBlocksCache = usersTableBriefBlocks;
    }

    @Override
    public void updateFieldsFromSrcForm(UserForm srcForm) {
        this.userName = srcForm.userName;
        this.userPass = srcForm.userPass;
        this.fullName = srcForm.fullName;
        this.sex = srcForm.sex;
        this.email = srcForm.email;

    }

    public boolean lock(long secs) {
        try {
            return lock.tryLock(secs, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            return false;
        }
    }
    
    public void unlock() {
        lock.unlock();
    }

    public class UsersRecipe implements Comparable<UsersRecipe> {
        private CookingDishRecipeForm recipe;
        private Map<Long, EatingMenuDishRecipeForm> eatingMenuRecipes;

        public UsersRecipe(CookingDishRecipeForm recipe) {
            this.recipe = recipe;
            eatingMenuRecipes = new HashMap<Long, EatingMenuDishRecipeForm>();
        }

        public CookingDishRecipeForm getRecipe() {
            return recipe;
        }

        public Map<Long, EatingMenuDishRecipeForm> getEatingMenuRecipes() {
            return eatingMenuRecipes;
        }

        @Override
        public int compareTo(UsersRecipe that) {
            return this.recipe.compareTo(that.recipe);
        }
    }

    public class UsersRecipeGroup implements Comparable<UsersRecipeGroup> {
        private CookingDishRecipeGroupForm group;
        private UsersRecipeGroup parent;
        private List<UsersRecipeGroup> childs;
        private List<CookingDishRecipeForm> recipes;
        private boolean childsSorted;
        private boolean recipesSorted;

        UsersRecipeGroup(CookingDishRecipeGroupForm group) {
            this.group = group;
        }

        public void setParent(UsersRecipeGroup parent) {
            this.parent = parent;
        }

        public void addChildGroup(UsersRecipeGroup child) {
            if (childs == null) {
                childs = new ArrayList<UsersRecipeGroup>();
            }
            childs.add(child);
            childsSorted = false;
        }

        public void addRecipe(CookingDishRecipeForm recipe) {
            if (recipes == null) {
                recipes = new ArrayList<CookingDishRecipeForm>();
            }
            recipes.add(recipe);
            recipesSorted = false;
        }

        public long getId() {
            return group.getId();
        }

        public String getName() {
            return group.getName();
        }

        public UsersRecipeGroup getParent() {
            return parent;
        }

        public List<UsersRecipeGroup> getChilds() {
            if (childs == null) {
                return Collections.emptyList();
            }
            if (!childsSorted) {
                Collections.sort(childs);
                childsSorted = true;
            }
            return childs;
        }

        public List<CookingDishRecipeForm> getRecipes() {
            if (recipes == null) {
                return Collections.emptyList();
            }
            if (!recipesSorted) {
                Collections.sort(recipes);
                recipesSorted = true;
            }
            return recipes;
        }

        @Override
        public int compareTo(UsersRecipeGroup that) {
            return this.group.compareTo(that.group);
        }

        /**
         * Method gets all recipes including recipes from subgroups
         * @return
         */
        public List<CookingDishRecipeForm> getAllRecipes() {
            List<CookingDishRecipeForm> result = new ArrayList<CookingDishRecipeForm>();
            result.addAll(getRecipes());
            for (UsersRecipeGroup child : getChilds()) {
                result.addAll(child.getAllRecipes());
            }
            Collections.sort(result);
            return result;
        }
    }

    public class UsersWare implements Comparable<UsersWare>{

        private WareForm ware;

        private WareRegionPriceForm wareRegionPrice;

        private WareUserPriceForm wareUserPrice;

        private int count;  // how much times this ware occurs in user recipes & recipes that user uses

        public UsersWare(WareForm ware) {
            this(ware, 1);
        }

        public UsersWare(WareForm ware, int count) {
            this.ware = ware;
            this.count = count;
        }

        public long getId() {
            return ware.getId();
        }

        public WareForm getWare() {
            return ware;
        }

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

        public void incCount(int inc) {
            count += inc;
        }

        public void decCount(int dec) {
            count -= dec;
        }

        public WareRegionPriceForm getWareRegionPrice() {
            return wareRegionPrice;
        }

        public String getWareRegionPriceStrValue() {
            return wareRegionPrice != null ? String.valueOf(wareRegionPrice.getPrice()) : "-";
        }

        public void setWareRegionPrice(WareRegionPriceForm wareRegionPrice) {
            this.wareRegionPrice = wareRegionPrice;
        }

        public WareUserPriceForm getWareUserPrice() {
            return wareUserPrice;
        }

        public String getWareUserPriceStrValue() {
            return wareUserPrice != null ? String.valueOf(wareUserPrice.getPrice()) : "-";
        }

        public void setWareUserPrice(WareUserPriceForm wareUserPrice) {
            this.wareUserPrice = wareUserPrice;
        }

        @Override
        public int compareTo(UsersWare that) {
            return this.ware.compareTo(that.ware);
        }
    }
}
